Skip to content

redact repository password from log output#114

Open
kinglike1337 wants to merge 1 commit into
lawndoc:mainfrom
kinglike1337:fix/redact-repo-password-in-logs
Open

redact repository password from log output#114
kinglike1337 wants to merge 1 commit into
lawndoc:mainfrom
kinglike1337:fix/redact-repo-password-in-logs

Conversation

@kinglike1337

Copy link
Copy Markdown

Summary

The restic repository URL can carry the password inline as HTTP basic auth, e.g.:

rest:http://user:password@host:8000/path

The password was leaking to the logs through two independent paths. This change redacts embedded credentials in all of them.

Leak paths fixed

1. Status logging (cli.status())

cli.status() logged config.repository verbatim at two INFO statements, leaking the secret to stdout — and therefore into the cron log (> /proc/1/fd/1) on every scheduled run.

Before:

INFO: Repository: 'rest:http://user:s3cr3t@host:8000/path'

After:

INFO: Repository: 'rest:http://user:***@host:8000/path'

2. Command logging (commands.run / run_capture_std)

Every restic invocation flows through commands.run()/run_capture_std(), which log the full command line:

logger.debug("cmd: %s", " ".join(cmd))

Since restic() builds ["restic", "-r", repository, ...], setting LOG_LEVEL=debug exposed the password on every backup / init / forget / prune / check / snapshots run. The fix is centralized: a new commands._redacted_cmd() masks each argument before logging, so all current and future call sites are covered automatically.

Changes

  • utils.redact_repo_url(repository) — masks any embedded password with ***, robust against passwords containing / or @ (handles rest:/http:/https: basic auth; the rest: backend prefix is preserved). Repository strings without credentials (s3:, local paths, key-based sftp:) are returned unchanged. (Note: restic's sftp backend authenticates via ssh keys/agent and has no inline URL password, so bare sftp: specs are intentionally not redacted.)
  • commands.py — route all command logging through _redacted_cmd().
  • cli.status() — wrap the two repository log statements in redact_repo_url().

The actual restic invocations (restic -r <repo> …) still receive the full, unredacted repository — only log output is masked.

Testing

  • tests/unit/test_repository_redaction.py — basic-auth redaction, https, passwords with / and @, credentials-free repos unchanged, secret never present in output.
  • tests/unit/test_command_redaction.pyrun() and run_capture_std() do not leak the password at DEBUG.
  • Full unit suite: 26 passed. Files pass black and ruff.

Backward compatibility

No change to backups or restic invocations — only log output is affected.

The restic repository URL can carry the password inline as HTTP basic
auth (rest:http://user:password@host:8000/path). This leaked the secret
in two ways:

- cli.status() logged config.repository verbatim at two INFO statements.
- commands.run()/run_capture_std() logged the full restic command line
  (including -r <repo>) at DEBUG, so LOG_LEVEL=debug exposed the password
  on every backup/init/forget/prune/check run.

Add utils.redact_repo_url(), which masks any embedded password (incl. "/"
or "@" characters) with ***, and route all command and repository logging
through it. Repository strings without credentials are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant